// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text;
using CommunityToolkit.HighPerformance.Helpers;
#if NET6_0_OR_GREATER
using BitOperations = System.Numerics.BitOperations;
#else
using BitOperations = CommunityToolkit.HighPerformance.Helpers.Internals.BitOperations;
#endif

namespace CommunityToolkit.HighPerformance.Buffers;

/// <summary>
/// A configurable pool for <see cref="string"/> instances. This can be used to minimize allocations
/// when creating multiple <see cref="string"/> instances from buffers of <see cref="char"/> values.
/// The <see cref="GetOrAdd(ReadOnlySpan{char})"/> method provides a best-effort alternative to just creating
/// a new <see cref="string"/> instance every time, in order to minimize the number of duplicated instances.
/// The <see cref="StringPool"/> type will internally manage a highly efficient priority queue for the
/// cached <see cref="string"/> instances, so that when the full capacity is reached, the least frequently
/// used values will be automatically discarded to leave room for new values to cache.
/// </summary>
public sealed class StringPool
{
    /// <summary>
    /// The size used by default by the parameterless constructor.
    /// </summary>
    private const int DefaultSize = 2048;

    /// <summary>
    /// The minimum size for <see cref="StringPool"/> instances.
    /// </summary>
    private const int MinimumSize = 32;

    /// <summary>
    /// The current array of <see cref="FixedSizePriorityMap"/> instances in use.
    /// </summary>
    private readonly FixedSizePriorityMap[] maps;

    /// <summary>
    /// The total number of maps in use.
    /// </summary>
    private readonly int numberOfMaps;

    /// <summary>
    /// Initializes a new instance of the <see cref="StringPool"/> class.
    /// </summary>
    public StringPool()
        : this(DefaultSize)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="StringPool"/> class.
    /// </summary>
    /// <param name="minimumSize">The minimum size for the pool to create.</param>
    public StringPool(int minimumSize)
    {
        if (minimumSize <= 0)
        {
            ThrowArgumentOutOfRangeException();
        }

        // Set the minimum size
        minimumSize = Math.Max(minimumSize, MinimumSize);

        // Calculates the rounded up factors for a specific size/factor pair
        static void FindFactors(int size, int factor, out uint x, out uint y)
        {
            double a = Math.Sqrt((double)size / factor);
            double b = factor * a;

            x = BitOperations.RoundUpToPowerOf2((uint)a);
            y = BitOperations.RoundUpToPowerOf2((uint)b);
        }

        // We want to find two powers of 2 factors that produce a number
        // that is at least equal to the requested size. In order to find the
        // combination producing the optimal factors (with the product being as
        // close as possible to the requested size), we test a number of ratios
        // that we consider acceptable, and pick the best results produced.
        // The ratio between maps influences the number of objects being allocated,
        // as well as the multithreading performance when locking on maps.
        // We still want to constraint this number to avoid situations where we
        // have a way too high number of maps compared to total size.
        FindFactors(minimumSize, 2, out uint x2, out uint y2);
        FindFactors(minimumSize, 3, out uint x3, out uint y3);
        FindFactors(minimumSize, 4, out uint x4, out uint y4);

        uint p2 = x2 * y2;
        uint p3 = x3 * y3;
        uint p4 = x4 * y4;

        if (p3 < p2)
        {
            p2 = p3;
            x2 = x3;
            y2 = y3;
        }

        if (p4 < p2)
        {
            p2 = p4;
            x2 = x4;
            y2 = y4;
        }

        Span<FixedSizePriorityMap> span = this.maps = new FixedSizePriorityMap[x2];

        // We preallocate the maps in advance, since each bucket only contains the
        // array field, which is not preinitialized, so the allocations are minimal.
        // This lets us lock on each individual map when retrieving a string instance.
        foreach (ref FixedSizePriorityMap map in span)
        {
            map = new FixedSizePriorityMap((int)y2);
        }

        this.numberOfMaps = (int)x2;

        Size = (int)p2;
    }

    /// <summary>
    /// Gets the shared <see cref="StringPool"/> instance.
    /// </summary>
    /// <remarks>
    /// The shared pool provides a singleton, reusable <see cref="StringPool"/> instance that
    /// can be accessed directly, and that pools <see cref="string"/> instances for the entire
    /// process. Since <see cref="StringPool"/> is thread-safe, the shared instance can be used
    /// concurrently by multiple threads without the need for manual synchronization.
    /// </remarks>
    public static StringPool Shared { get; } = new();

    /// <summary>
    /// Gets the total number of <see cref="string"/> that can be stored in the current instance.
    /// </summary>
    public int Size { get; }

    /// <summary>
    /// Stores a <see cref="string"/> instance in the internal cache.
    /// </summary>
    /// <param name="value">The input <see cref="string"/> instance to cache.</param>
    public void Add(string value)
    {
        if (value.Length == 0)
        {
            return;
        }

        int hashcode = GetHashCode(value.AsSpan());
        int bucketIndex = hashcode & (this.numberOfMaps - 1);

        ref FixedSizePriorityMap map = ref this.maps.DangerousGetReferenceAt(bucketIndex);

        lock (map.SyncRoot)
        {
            map.Add(value, hashcode);
        }
    }

    /// <summary>
    /// Gets a cached <see cref="string"/> instance matching the input content, or stores the input one.
    /// </summary>
    /// <param name="value">The input <see cref="string"/> instance with the contents to use.</param>
    /// <returns>A <see cref="string"/> instance with the contents of <paramref name="value"/>, cached if possible.</returns>
    public string GetOrAdd(string value)
    {
        if (value.Length == 0)
        {
            return string.Empty;
        }

        int hashcode = GetHashCode(value.AsSpan());
        int bucketIndex = hashcode & (this.numberOfMaps - 1);

        ref FixedSizePriorityMap map = ref this.maps.DangerousGetReferenceAt(bucketIndex);

        lock (map.SyncRoot)
        {
            return map.GetOrAdd(value, hashcode);
        }
    }

    /// <summary>
    /// Gets a cached <see cref="string"/> instance matching the input content, or creates a new one.
    /// </summary>
    /// <param name="span">The input <see cref="ReadOnlySpan{T}"/> with the contents to use.</param>
    /// <returns>A <see cref="string"/> instance with the contents of <paramref name="span"/>, cached if possible.</returns>
    public string GetOrAdd(ReadOnlySpan<char> span)
    {
        if (span.IsEmpty)
        {
            return string.Empty;
        }

        int hashcode = GetHashCode(span);
        int bucketIndex = hashcode & (this.numberOfMaps - 1);

        ref FixedSizePriorityMap map = ref this.maps.DangerousGetReferenceAt(bucketIndex);

        lock (map.SyncRoot)
        {
            return map.GetOrAdd(span, hashcode);
        }
    }

    /// <summary>
    /// Gets a cached <see cref="string"/> instance matching the input content (converted to Unicode), or creates a new one.
    /// </summary>
    /// <param name="span">The input <see cref="ReadOnlySpan{T}"/> with the contents to use, in a specified encoding.</param>
    /// <param name="encoding">The <see cref="Encoding"/> instance to use to decode the contents of <paramref name="span"/>.</param>
    /// <returns>A <see cref="string"/> instance with the contents of <paramref name="span"/>, cached if possible.</returns>
    public unsafe string GetOrAdd(ReadOnlySpan<byte> span, Encoding encoding)
    {
        if (span.IsEmpty)
        {
            return string.Empty;
        }

        int maxLength = encoding.GetMaxCharCount(span.Length);

        using SpanOwner<char> buffer = SpanOwner<char>.Allocate(maxLength);

        fixed (byte* source = span)
        fixed (char* destination = &buffer.DangerousGetReference())
        {
            int effectiveLength = encoding.GetChars(source, span.Length, destination, maxLength);

            return GetOrAdd(new ReadOnlySpan<char>(destination, effectiveLength));
        }
    }

    /// <summary>
    /// Tries to get a cached <see cref="string"/> instance matching the input content, if present.
    /// </summary>
    /// <param name="span">The input <see cref="ReadOnlySpan{T}"/> with the contents to use.</param>
    /// <param name="value">The resulting cached <see cref="string"/> instance, if present</param>
    /// <returns>Whether or not the target <see cref="string"/> instance was found.</returns>
    public bool TryGet(ReadOnlySpan<char> span, [NotNullWhen(true)] out string? value)
    {
        if (span.IsEmpty)
        {
            value = string.Empty;

            return true;
        }

        int hashcode = GetHashCode(span);
        int bucketIndex = hashcode & (this.numberOfMaps - 1);

        ref FixedSizePriorityMap map = ref this.maps.DangerousGetReferenceAt(bucketIndex);

        lock (map.SyncRoot)
        {
            return map.TryGet(span, hashcode, out value);
        }
    }

    /// <summary>
    /// Resets the current instance and its associated maps.
    /// </summary>
    public void Reset()
    {
        foreach (ref FixedSizePriorityMap map in this.maps.AsSpan())
        {
            lock (map.SyncRoot)
            {
                map.Reset();
            }
        }
    }

    /// <summary>
    /// A configurable map containing a group of cached <see cref="string"/> instances.
    /// </summary>
    /// <remarks>
    /// Instances of this type are stored in an array within <see cref="StringPool"/> and they are
    /// always accessed by reference - essentially as if this type had been a class. The type is
    /// also private, so there's no risk for users to directly access it and accidentally copy an
    /// instance, which would lead to bugs due to values becoming out of sync with the internal state
    /// (that is, because instances would be copied by value, so primitive fields would not be shared).
    /// The reason why we're using a struct here is to remove an indirection level and improve cache
    /// locality when accessing individual buckets from the methods in the <see cref="StringPool"/> type.
    /// </remarks>
    private struct FixedSizePriorityMap
    {
        /// <summary>
        /// The index representing the end of a given list.
        /// </summary>
        private const int EndOfList = -1;

        /// <summary>
        /// The array of 1-based indices for the <see cref="MapEntry"/> items stored in <see cref="mapEntries"/>.
        /// </summary>
        private readonly int[] buckets;

        /// <summary>
        /// The array of currently cached entries (ie. the lists for each hash group).
        /// </summary>
        private readonly MapEntry[] mapEntries;

        /// <summary>
        /// The array of priority values associated to each item stored in <see cref="mapEntries"/>.
        /// </summary>
        private readonly HeapEntry[] heapEntries;

        /// <summary>
        /// The current number of items stored in the map.
        /// </summary>
        private int count;

        /// <summary>
        /// The current incremental timestamp for the items stored in <see cref="heapEntries"/>.
        /// </summary>
        private uint timestamp;

        /// <summary>
        /// A type representing a map entry, ie. a node in a given list.
        /// </summary>
        private struct MapEntry
        {
            /// <summary>
            /// The precomputed hashcode for <see cref="Value"/>.
            /// </summary>
            public int HashCode;

            /// <summary>
            /// The <see cref="string"/> instance cached in this entry.
            /// </summary>
            public string? Value;

            /// <summary>
            /// The 0-based index for the next node in the current list.
            /// </summary>
            public int NextIndex;

            /// <summary>
            /// The 0-based index for the heap entry corresponding to the current node.
            /// </summary>
            public int HeapIndex;
        }

        /// <summary>
        /// A type representing a heap entry, used to associate priority to each item.
        /// </summary>
        private struct HeapEntry
        {
            /// <summary>
            /// The timestamp for the current entry (ie. the priority for the item).
            /// </summary>
            public uint Timestamp;

            /// <summary>
            /// The 0-based index for the map entry corresponding to the current item.
            /// </summary>
            public int MapIndex;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="FixedSizePriorityMap"/> struct.
        /// </summary>
        /// <param name="capacity">The fixed capacity of the current map.</param>
        public FixedSizePriorityMap(int capacity)
        {
            this.buckets = new int[capacity];
            this.mapEntries = new MapEntry[capacity];
            this.heapEntries = new HeapEntry[capacity];
            this.count = 0;
            this.timestamp = 0;
        }

        /// <summary>
        /// Gets an <see cref="object"/> that can be used to synchronize access to the current instance.
        /// </summary>
        public object SyncRoot
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get => this.buckets;
        }

        /// <summary>
        /// Implements <see cref="StringPool.Add"/> for the current instance.
        /// </summary>
        /// <param name="value">The input <see cref="string"/> instance to cache.</param>
        /// <param name="hashcode">The precomputed hashcode for <paramref name="value"/>.</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Add(string value, int hashcode)
        {
            ref string target = ref TryGet(value.AsSpan(), hashcode);

            if (Unsafe.IsNullRef(ref target))
            {
                Insert(value, hashcode);
            }
            else
            {
                target = value;
            }
        }

        /// <summary>
        /// Implements <see cref="StringPool.GetOrAdd(string)"/> for the current instance.
        /// </summary>
        /// <param name="value">The input <see cref="string"/> instance with the contents to use.</param>
        /// <param name="hashcode">The precomputed hashcode for <paramref name="value"/>.</param>
        /// <returns>A <see cref="string"/> instance with the contents of <paramref name="value"/>.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public string GetOrAdd(string value, int hashcode)
        {
            ref string result = ref TryGet(value.AsSpan(), hashcode);

            if (!Unsafe.IsNullRef(ref result))
            {
                return result;
            }

            Insert(value, hashcode);

            return value;
        }

        /// <summary>
        /// Implements <see cref="StringPool.GetOrAdd(ReadOnlySpan{char})"/> for the current instance.
        /// </summary>
        /// <param name="span">The input <see cref="ReadOnlySpan{T}"/> with the contents to use.</param>
        /// <param name="hashcode">The precomputed hashcode for <paramref name="span"/>.</param>
        /// <returns>A <see cref="string"/> instance with the contents of <paramref name="span"/>, cached if possible.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public string GetOrAdd(ReadOnlySpan<char> span, int hashcode)
        {
            ref string result = ref TryGet(span, hashcode);

            if (!Unsafe.IsNullRef(ref result))
            {
                return result;
            }

            string value = span.ToString();

            Insert(value, hashcode);

            return value;
        }

        /// <summary>
        /// Implements <see cref="StringPool.TryGet"/> for the current instance.
        /// </summary>
        /// <param name="span">The input <see cref="ReadOnlySpan{T}"/> with the contents to use.</param>
        /// <param name="hashcode">The precomputed hashcode for <paramref name="span"/>.</param>
        /// <param name="value">The resulting cached <see cref="string"/> instance, if present</param>
        /// <returns>Whether or not the target <see cref="string"/> instance was found.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool TryGet(ReadOnlySpan<char> span, int hashcode, [NotNullWhen(true)] out string? value)
        {
            ref string result = ref TryGet(span, hashcode);

            if (!Unsafe.IsNullRef(ref result))
            {
                value = result;

                return true;
            }

            value = null;

            return false;
        }

        /// <summary>
        /// Resets the current instance and throws away all the cached values.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Reset()
        {
            this.buckets.AsSpan().Clear();
            this.mapEntries.AsSpan().Clear();
            this.heapEntries.AsSpan().Clear();
            this.count = 0;
            this.timestamp = 0;
        }

        /// <summary>
        /// Tries to get a target <see cref="string"/> instance, if it exists, and returns a reference to it.
        /// </summary>
        /// <param name="span">The input <see cref="ReadOnlySpan{T}"/> with the contents to use.</param>
        /// <param name="hashcode">The precomputed hashcode for <paramref name="span"/>.</param>
        /// <returns>A reference to the slot where the target <see cref="string"/> instance could be.</returns>
        [MethodImpl(MethodImplOptions.NoInlining)]
        private unsafe ref string TryGet(ReadOnlySpan<char> span, int hashcode)
        {
            ref MapEntry mapEntriesRef = ref this.mapEntries.DangerousGetReference();
            ref MapEntry entry = ref *(MapEntry*)null;
            int length = this.buckets.Length;
            int bucketIndex = hashcode & (length - 1);

            for (int i = this.buckets.DangerousGetReferenceAt(bucketIndex) - 1;
                 (uint)i < (uint)length;
                 i = entry.NextIndex)
            {
                entry = ref Unsafe.Add(ref mapEntriesRef, (nint)(uint)i);

                if (entry.HashCode == hashcode &&
                    entry.Value!.AsSpan().SequenceEqual(span))
                {
                    UpdateTimestamp(ref entry.HeapIndex);

                    return ref entry.Value!;
                }
            }

            return ref *(string*)null;
        }

        /// <summary>
        /// Inserts a new <see cref="string"/> instance in the current map, freeing up a space if needed.
        /// </summary>
        /// <param name="value">The new <see cref="string"/> instance to store.</param>
        /// <param name="hashcode">The precomputed hashcode for <paramref name="value"/>.</param>
        [MethodImpl(MethodImplOptions.NoInlining)]
        private void Insert(string value, int hashcode)
        {
            ref int bucketsRef = ref this.buckets.DangerousGetReference();
            ref MapEntry mapEntriesRef = ref this.mapEntries.DangerousGetReference();
            ref HeapEntry heapEntriesRef = ref this.heapEntries.DangerousGetReference();
            int entryIndex, heapIndex;

            // If the current map is full, first get the oldest value, which is
            // always the first item in the heap. Then, free up a slot by
            // removing that, and insert the new value in that empty location.
            if (this.count == this.mapEntries.Length)
            {
                entryIndex = heapEntriesRef.MapIndex;
                heapIndex = 0;

                ref MapEntry removedEntry = ref Unsafe.Add(ref mapEntriesRef, (nint)(uint)entryIndex);

                // The removal logic can be extremely optimized in this case, as we
                // can retrieve the precomputed hashcode for the target entry by doing
                // a lookup on the target map node, and we can also skip all the comparisons
                // while traversing the target chain since we know in advance the index of
                // the target node which will contain the item to remove from the map.
                Remove(removedEntry.HashCode, entryIndex);
            }
            else
            {
                // If the free list is not empty, get that map node and update the field
                entryIndex = this.count;
                heapIndex = this.count;
            }

            int bucketIndex = hashcode & (this.buckets.Length - 1);
            ref int targetBucket = ref Unsafe.Add(ref bucketsRef, (nint)(uint)bucketIndex);
            ref MapEntry targetMapEntry = ref Unsafe.Add(ref mapEntriesRef, (nint)(uint)entryIndex);
            ref HeapEntry targetHeapEntry = ref Unsafe.Add(ref heapEntriesRef, (nint)(uint)heapIndex);

            // Assign the values in the new map entry
            targetMapEntry.HashCode = hashcode;
            targetMapEntry.Value = value;
            targetMapEntry.NextIndex = targetBucket - 1;
            targetMapEntry.HeapIndex = heapIndex;

            // Update the bucket slot and the current count
            targetBucket = entryIndex + 1;
            this.count++;

            // Link the heap node with the current entry
            targetHeapEntry.MapIndex = entryIndex;

            // Update the timestamp and balance the heap again
            UpdateTimestamp(ref targetMapEntry.HeapIndex);
        }

        /// <summary>
        /// Removes a specified <see cref="string"/> instance from the map to free up one slot.
        /// </summary>
        /// <param name="hashcode">The precomputed hashcode of the instance to remove.</param>
        /// <param name="mapIndex">The index of the target map node to remove.</param>
        /// <remarks>The input <see cref="string"/> instance needs to already exist in the map.</remarks>
        [MethodImpl(MethodImplOptions.NoInlining)]
        private void Remove(int hashcode, int mapIndex)
        {
            ref MapEntry mapEntriesRef = ref this.mapEntries.DangerousGetReference();
            int bucketIndex = hashcode & (this.buckets.Length - 1);
            int entryIndex = this.buckets.DangerousGetReferenceAt(bucketIndex) - 1;
            int lastIndex = EndOfList;

            // We can just have an undefined loop, as the input
            // value we're looking for is guaranteed to be present
            while (true)
            {
                ref MapEntry candidate = ref Unsafe.Add(ref mapEntriesRef, (nint)(uint)entryIndex);

                // Check the current value for a match
                if (entryIndex == mapIndex)
                {
                    // If this was not the first list node, update the parent as well
                    if (lastIndex != EndOfList)
                    {
                        ref MapEntry lastEntry = ref Unsafe.Add(ref mapEntriesRef, (nint)(uint)lastIndex);

                        lastEntry.NextIndex = candidate.NextIndex;
                    }
                    else
                    {
                        // Otherwise, update the target index from the bucket slot
                        this.buckets.DangerousGetReferenceAt(bucketIndex) = candidate.NextIndex + 1;
                    }

                    this.count--;

                    return;
                }

                // Move to the following node in the current list
                lastIndex = entryIndex;
                entryIndex = candidate.NextIndex;
            }
        }

        /// <summary>
        /// Updates the timestamp of a heap node at the specified index (which is then synced back).
        /// </summary>
        /// <param name="heapIndex">The index of the target heap node to update.</param>
        [MethodImpl(MethodImplOptions.NoInlining)]
        private void UpdateTimestamp(ref int heapIndex)
        {
            int currentIndex = heapIndex;
            int count = this.count;
            ref MapEntry mapEntriesRef = ref this.mapEntries.DangerousGetReference();
            ref HeapEntry heapEntriesRef = ref this.heapEntries.DangerousGetReference();
            ref HeapEntry root = ref Unsafe.Add(ref heapEntriesRef, (nint)(uint)currentIndex);
            uint timestamp = this.timestamp;

            // Check if incrementing the current timestamp for the heap node to update
            // would result in an overflow. If that happened, we could end up violating
            // the min-heap property (the value of each node has to always be <= than that
            // of its child nodes), eg. if we were updating a node that was not the root.
            // In that scenario, we could end up with a node somewhere along the heap with
            // a value lower than that of its parent node (as the timestamp would be 0).
            // To guard against this, we just check the current timestamp value, and if
            // the maximum value has been reached, we reinitialize the entire heap. This
            // is done in a non-inlined call, so we don't increase the codegen size in this
            // method. The reinitialization simply traverses the heap in breadth-first order
            // (ie. level by level), and assigns incrementing timestamps to all nodes starting
            // from 0. The value of the current timestamp is then just set to the current size.
            if (timestamp == uint.MaxValue)
            {
                // We use a goto here as this path is very rarely taken. Doing so
                // causes the generated asm to contain a forward jump to the fallback
                // path if this branch is taken, whereas the normal execution path will
                // not need to execute any jumps at all. This is done to reduce the overhead
                // introduced by this check in all the invocations where this point is not reached.
                goto Fallback;
            }

            Downheap:

            // Assign a new timestamp to the target heap node. We use a
            // local incremental timestamp instead of using the system timer
            // as this greatly reduces the overhead and the time spent in system calls.
            // The uint type provides a large enough range and it's unlikely users would ever
            // exhaust it anyway (especially considering each map has a separate counter).
            root.Timestamp = this.timestamp = timestamp + 1;

            // Once the timestamp is updated (which will cause the heap to become
            // unbalanced), start a sift down loop to balance the heap again.
            while (true)
            {
                // The heap is 0-based (so that the array length can remain the same
                // as the power of 2 value used for the other arrays in this type).
                // This means that children of each node are at positions:
                //   - left: (2 * n) + 1
                //   - right: (2 * n) + 2
                ref HeapEntry minimum = ref root;
                int left = (currentIndex * 2) + 1;
                int right = (currentIndex * 2) + 2;
                int targetIndex = currentIndex;

                // Check and update the left child, if necessary
                if (left < count)
                {
                    ref HeapEntry child = ref Unsafe.Add(ref heapEntriesRef, (nint)(uint)left);

                    if (child.Timestamp < minimum.Timestamp)
                    {
                        minimum = ref child;
                        targetIndex = left;
                    }
                }

                // Same check as above for the right child
                if (right < count)
                {
                    ref HeapEntry child = ref Unsafe.Add(ref heapEntriesRef, (nint)(uint)right);

                    if (child.Timestamp < minimum.Timestamp)
                    {
                        minimum = ref child;
                        targetIndex = right;
                    }
                }

                // If no swap is pending, we can just stop here.
                // Before returning, we update the target index as well.
                if (Unsafe.AreSame(ref root, ref minimum))
                {
                    heapIndex = targetIndex;

                    return;
                }

                // Update the indices in the respective map entries (accounting for the swap)
                Unsafe.Add(ref mapEntriesRef, (nint)(uint)root.MapIndex).HeapIndex = targetIndex;
                Unsafe.Add(ref mapEntriesRef, (nint)(uint)minimum.MapIndex).HeapIndex = currentIndex;

                currentIndex = targetIndex;

                // Swap the parent and child (so that the minimum value bubbles up)
                HeapEntry temp = root;

                root = minimum;
                minimum = temp;

                // Update the reference to the root node
                root = ref Unsafe.Add(ref heapEntriesRef, (nint)(uint)currentIndex);
            }

            Fallback:

            UpdateAllTimestamps();

            // After having updated all the timestamps, if the heap contains N items, the
            // node in the bottom right corner will have a value of N - 1. Since the timestamp
            // is incremented by 1 before starting the downheap execution, here we simply
            // update the local timestamp to be N - 1, so that the code above will set the
            // timestamp of the node currently being updated to exactly N.
            timestamp = (uint)(count - 1);

            goto Downheap;
        }

        /// <summary>
        /// Updates the timestamp of all the current heap nodes in incrementing order.
        /// The heap is always guaranteed to be complete binary tree, so when it contains
        /// a given number of nodes, those are all contiguous from the start of the array.
        /// </summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        private void UpdateAllTimestamps()
        {
            int count = this.count;
            ref HeapEntry heapEntriesRef = ref this.heapEntries.DangerousGetReference();

            for (int i = 0; i < count; i++)
            {
                Unsafe.Add(ref heapEntriesRef, (nint)(uint)i).Timestamp = (uint)i;
            }
        }
    }

    /// <summary>
    /// Gets the (positive) hashcode for a given <see cref="ReadOnlySpan{T}"/> instance.
    /// </summary>
    /// <param name="span">The input <see cref="ReadOnlySpan{T}"/> instance.</param>
    /// <returns>The hashcode for <paramref name="span"/>.</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static int GetHashCode(ReadOnlySpan<char> span)
    {
        return HashCode<char>.Combine(span);
    }

    /// <summary>
    /// Throws an <see cref="ArgumentException"/> when the requested size exceeds the capacity.
    /// </summary>
    private static void ThrowArgumentOutOfRangeException()
    {
        throw new ArgumentOutOfRangeException("minimumSize", "The requested size must be greater than 0");
    }
}
